package com.wr.hangzhounewlight.service;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.wr.hangzhounewlight.mapper.ChangeLogMapper;
import com.wr.hangzhounewlight.mapper.OrderMapper;
import com.wr.hangzhounewlight.mapper.RecipeMapper;
import com.wr.hangzhounewlight.obj.database.ChangeLogEntity;
import com.wr.hangzhounewlight.obj.database.OrderEntity;
import com.wr.hangzhounewlight.obj.database.RecipeEntity;
import com.wr.hangzhounewlight.opcua.OpcUaClientManager;
import com.wr.hangzhounewlight.opcua.OpcUaUtil;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem.DataValueListener;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription.SubscriptionListener;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.IntStream;

/**
 * @author yuanzhizhuo
 * createDate 2025/11/4
 */
@Slf4j
@Service
public class RecipeCheckListenerService {

    private final OpcUaClientManager opcUaClientManager;
    private final OrderMapper orderMapper;
    private final RecipeMapper recipeMapper;
    private final ChangeLogMapper changeLogMapper;
    private final String opcUaUrl = "opc.tcp://frp-fog.com:48347";

    private static final Map<String, String> T = new HashMap<>();

    static {
        IntStream.rangeClosed(1, 60).forEach(i -> T.put("主料" + i, "SER_RCP" + "_" + String.format("%03d", i)));
    }

    public RecipeCheckListenerService(
            OpcUaClientManager opcUaClientManager, OrderMapper orderMapper, RecipeMapper recipeMapper, ChangeLogMapper changeLogMapper
    ) {
        this.opcUaClientManager = opcUaClientManager;
        this.orderMapper = orderMapper;
        this.recipeMapper = recipeMapper;
        this.changeLogMapper = changeLogMapper;
    }

    @PostConstruct
    private void addOpcUaNodeIdListener() {
        Optional<OpcUaClient> opcUaClientOptional = opcUaClientManager.getOpcUaClient(opcUaUrl);
        if (opcUaClientOptional.isPresent()) {
            OpcUaClient opcUaClient = opcUaClientOptional.get();
            try {
                List<NodeId> nodeIdList = OpcUaUtil.browseNode(opcUaClient, new NodeId(0, 53722));
                List<NodeId> listenerNodeIdList = getListenerNodeIdList(nodeIdList);
                List<DataValueListener> dataValueListenerList = Collections.nCopies(
                        listenerNodeIdList.size(), new MyDataValueListener(opcUaClient, orderMapper, recipeMapper, changeLogMapper)
                );
                OpcUaUtil.subscribeDataValue(opcUaClient, new MySubscriptionListener(), listenerNodeIdList, dataValueListenerList);
                log.info("配方校验, 已添加监听点位: {}", listenerNodeIdList);
            } catch (ExecutionException | InterruptedException | TimeoutException | UaException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 过滤出所有需要监听的NodeId
     *
     * @param nodeIdList 所有节点的NodeId列表
     * @return 需要监听的NodeId列表
     */
    private static List<NodeId> getListenerNodeIdList(List<NodeId> nodeIdList) {
        return nodeIdList.stream().filter(nodeId -> String.valueOf(nodeId.getIdentifier()).contains("DownloadCheck")).toList();
    }

    /**
     * 监听器级别回调
     */
    private static class MySubscriptionListener implements SubscriptionListener {
        @Override
        public void onDataReceived(OpcUaSubscription subscription, List<OpcUaMonitoredItem> items, List<DataValue> values) {
            for (int i = 0; i < items.size(); i++) {
                OpcUaMonitoredItem item = items.get(i);
                DataValue value = values.get(i);
                log.info("配方校验, 监听器事件, 监听点位: {}发生变化, 新值: {}", item.getReadValueId().getNodeId(), value.getValue().getValue());
            }
        }
    }

    private static class MyDataValueListener implements DataValueListener {

        private final OpcUaClient opcUaClient;
        private final OrderMapper orderMapper;
        private final RecipeMapper recipeMapper;
        private final ChangeLogMapper changeLogMapper;
        private String workshop;
        private String machine;
        private String screw;

        public MyDataValueListener(
                OpcUaClient opcUaClient, OrderMapper orderMapper, RecipeMapper recipeMapper, ChangeLogMapper changeLogMapper
        ) {
            this.opcUaClient = opcUaClient;
            this.orderMapper = orderMapper;
            this.recipeMapper = recipeMapper;
            this.changeLogMapper = changeLogMapper;
        }

        /**
         * identifier中带有DownloadCheck关键字的节点监听器
         * <p>
         * 完整格式[workshop]_[machine]_[screw]_DownloadCheck
         * <p>
         * 检查数据库中的orderNo是否已校验
         * 只要点击下载配方按钮, 就必须记录变更日志
         * --如果已校验更新PF_OK按钮, 后续的所有操作都无需校验只记录变更日志
         * --如果未校验, 读取点位上的实际值, 和数据库中的配方进行对比
         * ----如果校验不通过, 更新数据库中组合单号对应的recipe_is_check为0
         * ----如果校验通过, 更新数据库中组合单号对应的recipe_is_check为1和对应的实际值, 并更新PF_OK按钮为true
         * 恢复下载配方按钮
         */
        @Override
        public void onDataReceived(OpcUaMonitoredItem item, DataValue value) {
            NodeId nodeId = item.getReadValueId().getNodeId();
            if (!Boolean.parseBoolean(String.valueOf(value.getValue().getValue()))) return;
            String[] identifierSplit = String.valueOf(nodeId.getIdentifier()).split("_");
            workshop = identifierSplit[0];
            machine = identifierSplit[1];
            screw = identifierSplit[2];
            boolean isCheckPass;
            String checkMessage;
            OrderNoCheckRecord orderNoRecipeCheckRecord = orderNoRecipeIsCheck();
            if (orderNoRecipeCheckRecord == null) return;
            String orderNo = orderNoRecipeCheckRecord.orderNo();
            ReadOrderNoRatioActValueRecord readOrderNoRatioActValueRecord = readOrderNoRatioActValue(orderNo);
            List<RecipeEntity> recipeEntityList = readOrderNoRatioActValueRecord.recipeEntityList();
            List<Double> ratioActList = readOrderNoRatioActValueRecord.ratioActList();
            if (orderNoRecipeCheckRecord.isCheck()) {
                log.info("配方校验, 组合单号: {}已校验", orderNo);
                isCheckPass = true;
                checkMessage = "已校验";
                updateRecipeAct(orderNo, true, recipeEntityList, ratioActList);
            } else {
                log.info("配方校验, 组合单号: {}未校验", orderNo);
                isCheckPass = checkRatioActValue(orderNo, recipeEntityList, ratioActList);
                if (isCheckPass) {
                    log.info("配方校验, 组合单号: {}校验通过", orderNo);
                    checkMessage = "校验通过";
                    updateRecipeAct(orderNo, true, recipeEntityList, ratioActList);
                } else {
                    log.info("配方校验, 组合单号: {}校验不通过", orderNo);
                    checkMessage = "校验不通过";
                    updateRecipeAct(orderNo, false, recipeEntityList, Collections.nCopies(recipeEntityList.size(), null));
                }
            }
            writeOrderNoIsCheck(isCheckPass);
            insertChangeLog(orderNo, checkMessage, ratioActList);
            resetDownloadRecipeButton();
        }

        /**
         * 读取组合单号对应的配方比例点位上的实际值
         *
         * @param orderNo 组合单号
         * @return 配方比例实际值
         */
        private ReadOrderNoRatioActValueRecord readOrderNoRatioActValue(String orderNo) {
            List<RecipeEntity> recipeEntityList = recipeMapper.selectRecipe(orderNo);
            List<NodeId> ratioNodeIdList = recipeEntityList.stream().map(entity -> {
                String nodeIdIdentifier = workshop + "_" + machine + "_" + screw + "_" + T.get(entity.getSilo());
                return new NodeId(1, nodeIdIdentifier);
            }).toList();
            try {
                List<Double> ratioActList = OpcUaUtil.readValue(opcUaClient, ratioNodeIdList).stream().map(v ->
                        Double.parseDouble(String.format("%.3f", Double.parseDouble(String.valueOf(v.getValue().getValue()))))
                ).toList();
                return new ReadOrderNoRatioActValueRecord(recipeEntityList, ratioActList);
            } catch (ExecutionException | InterruptedException | TimeoutException e) {
                log.error("配方校验, 组合单号: {}读取配方比例点位失败", orderNo, e);
                return new ReadOrderNoRatioActValueRecord(recipeEntityList, List.of());
            }
        }

        private record ReadOrderNoRatioActValueRecord(List<RecipeEntity> recipeEntityList, List<Double> ratioActList) {}

        /**
         * 记录配方变更日志
         *
         * @param checkMessage 校验信息
         * @param ratioActList 配方比例实际值
         */
        private void insertChangeLog(String orderNo, String checkMessage, List<Double> ratioActList) {
            if (ratioActList == null || ratioActList.isEmpty()) return;
            Map<String, Object> log = new LinkedHashMap<>() {{
                put("组合单号", orderNo);
                put("校验信息", checkMessage);
                put("配方比例实际值", ratioActList);
            }};
            changeLogMapper.insert(new ChangeLogEntity(orderNo, checkMessage, JSON.toJSONString(log, JSONWriter.Feature.PrettyFormat)));
        }

        /**
         * 检查组合单号的配方是否已校验
         *
         * @return 组合单号是否已校验
         */
        private OrderNoCheckRecord orderNoRecipeIsCheck() {
            String orderNoNodeIdIdentifier = workshop + "_" + machine + "_" + screw + "_curWO";
            NodeId orderNoNodeId = new NodeId(1, orderNoNodeIdIdentifier);
            try {
                String orderNo = String.valueOf(opcUaClient.readValue(0.0, TimestampsToReturn.Both, orderNoNodeId).getValue().getValue());
                LambdaQueryWrapper<OrderEntity> queryWrapper = new LambdaQueryWrapper<OrderEntity>()
                        .select(OrderEntity::getRecipeIsCheck)
                        .eq(OrderEntity::getOrderNo, orderNo);
                OrderEntity orderEntity = orderMapper.selectOne(queryWrapper);
                if (orderEntity == null) {
                    log.info("配方校验, 组合单号: {}不存在", orderNo);
                    return null;
                }
                Integer recipeIsCheck = orderEntity.getRecipeIsCheck();
                return new OrderNoCheckRecord(recipeIsCheck == 1, orderNo);
            } catch (UaException e) {
                log.info("配方校验, 读取组合单号点位: {}失败, 错误信息: {}", orderNoNodeId, e.getMessage());
                return new OrderNoCheckRecord(false, "");
            }
        }

        private record OrderNoCheckRecord(boolean isCheck, String orderNo) {}

        /**
         * 对比点位值和数据库中的值
         * <p>
         * 如果点位值是0, null, 或者相差大于0.001就检查失败
         * 如果有一个点位不通过就整体检查失败
         *
         * @param orderNo          组合单号
         * @param recipeEntityList 配方数据库实体列表
         * @param ratioActList     配方比例实际值
         * @return 是否检查通过
         */
        private boolean checkRatioActValue(String orderNo, List<RecipeEntity> recipeEntityList, List<Double> ratioActList) {
            for (int i = 0; i < recipeEntityList.size(); i++) {
                Double ratioAct = ratioActList.get(i);
                if (ratioAct == null || ratioAct == 0.0) {
                    log.info("配方校验, 组合单号: {}校验失败, 节点: {}值为null或0.0", orderNo, ratioAct);
                    return false;
                }
                Double ratio = recipeEntityList.get(i).getRatio();
                if (Math.abs(ratioAct - ratio) > 0.001) {
                    log.info("配方校验, 组合单号: {}校验失败, 节点值: {}, 与数据库中值: {}相差大于0.001", orderNo, ratioAct, ratio);
                    return false;
                }
            }
            return true;
        }

        /**
         * 更新数据库中的配方比例实际值
         *
         * @param orderNo           组合单号
         * @param recipeIsCheckPass 配方是否校验通过
         * @param recipeEntityList  配方数据库实体列表
         * @param ratioActList      配方比例实际值列表
         */
        private void updateRecipeAct(String orderNo, boolean recipeIsCheckPass, List<RecipeEntity> recipeEntityList, List<Double> ratioActList) {
            if (recipeEntityList.size() != ratioActList.size()) {
                log.info("配方校验, 更新数据库配方实际值, 点位值数量与配方数量不一致, 点位值数量: {}, 数据库值数量: {}", ratioActList.size(), recipeEntityList.size());
                return;
            }
            LambdaUpdateWrapper<OrderEntity> updateOrderNoWrapper = new LambdaUpdateWrapper<OrderEntity>()
                    .set(OrderEntity::getRecipeIsCheck, recipeIsCheckPass)
                    .eq(OrderEntity::getOrderNo, orderNo);
            orderMapper.update(updateOrderNoWrapper);
            for (int i = 0; i < recipeEntityList.size(); i++) {
                LambdaUpdateWrapper<RecipeEntity> updateWrapper = new LambdaUpdateWrapper<RecipeEntity>()
                        .set(RecipeEntity::getRatioAct, ratioActList.get(i))
                        .eq(RecipeEntity::getId, recipeEntityList.get(i).getId());
                recipeMapper.update(updateWrapper);
            }
        }

        private void writeOrderNoIsCheck(boolean isCheck) {
            String orderNoIsCheckNodeIdentifier = workshop + "_" + machine + "_" + screw + "_PF_OK";
            try {
                OpcUaUtil.writeValue(opcUaClient, new NodeId(1, orderNoIsCheckNodeIdentifier), isCheck);
            } catch (UaException e) {
                log.info("配方校验, 写入点位: {}失败, 错误信息: {}", orderNoIsCheckNodeIdentifier, e.getMessage());
            }
        }

        private void resetDownloadRecipeButton() {
            String NodeIdIdentifier = workshop + "_" + machine + "_" + screw + "_DownloadCheck";
            try {
                OpcUaUtil.writeValue(opcUaClient, new NodeId(1, NodeIdIdentifier), false);
            } catch (UaException e) {
                log.info("配方校验, 重置下载配方按钮失败, 点位: {}, 错误信息: {}", NodeIdIdentifier, e.getMessage());
            }
        }

    }

}
